﻿using System;
using System.ComponentModel;
using System.Security.Principal;
using System.Runtime.InteropServices;

namespace SBPweb.RIA.Client.Common.Security
{
    /// <summary>
    /// This class manages the creation of an impersonation to the Application
    ///  Pool's context for a defined scope.
    ///
    /// It just wraps the .Net class for impersonation, thus allowing easier
    ///  use.
    ///
    /// This class is made for use with the C# 'using' keyword, as in the
    ///  example below:
    /// <code>
    ///             using(Identity impersonate = Identity.ImpersonateAppPoolIdentity())
    ///             {
    ///                     // ... Do whatever job you want as the AppPool
    ///             }
    ///             // Back to the original, you don't have to worry about reverting,
    ///             // this is automatically done with the IDisposable interface
    /// </code>
    /// </summary>
    public sealed class Identity : IDisposable
    {
        /// <summary> 
        /// Windows identity used for the Application Pool 
        /// </summary> 
        private static WindowsIdentity _appPoolIdentity;

        /// <summary> 
        /// Gets the windows identity used for the Application Pool 
        /// </summary> 
        public static WindowsIdentity AppPoolIdentity
        {
            get
            {
                // Lock current type to ensure thread safety on   
                //  identity creation.   
                lock (typeof(Identity))
                {
                    if (_appPoolIdentity == null)
                    {
                        // Create a new handle from this one     
                        IntPtr token = WindowsIdentity.GetCurrent().Token;
                        // Throw an exception if we have an empty token     
                        if (token == IntPtr.Zero)
                        {
                            throw new ApplicationException("Unable to fetch AppPool's identity token !");
                        }
                        // Create a duplicate of the user's token in order to use it for impersonation     
                        if (!DuplicateToken(token, 2, ref token))
                        {
                            throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to duplicate AppPool's identity token !");
                        }
                        // Throw an exception if we were unable to duplicate the token     
                        if (token == IntPtr.Zero)
                        {
                            throw new ApplicationException("Unable to duplicate AppPool's identity token !");
                        }
                        // Store app pool's identity     
                        _appPoolIdentity = new WindowsIdentity(token);
                        // Free the windows unmanaged resource     
                        CloseHandle(token);
                    }
                    return _appPoolIdentity;
                }
            }
        }

        /// <summary> 
        /// This function returns the current user's login name. 
        /// </summary> 
        public static string CurrentUserName
        {
            get { return WindowsIdentity.GetCurrent().Name; }
        }

        /// <summary> 
        /// Stores the currently available Windows Impersonation context. 
        /// </summary> 
        private WindowsImpersonationContext _context;

        /// <summary> 
        /// Stores the app pool's identity context. 
        /// </summary> 
        private WindowsImpersonationContext _selfContext;

        private Identity()
            : this(true)
        {
        }

        /// <summary> 
        /// Private constructor, static function accessed class. 
        /// </summary> 
        private Identity(bool impersonate)
        {
            if (impersonate == false)
            {
                return;
            }

            // Try catch structure to ensure we don't change context in case  
            //  we had an error duplicating the token.  
            try
            {
                _selfContext = WindowsIdentity.Impersonate(IntPtr.Zero);
                // REVERT to AppPool identity!   
                _context = AppPoolIdentity.Impersonate();
            }
            catch
            {
                // Close the context   
                UndoImpersonation();
                // Rethrow the exception   
                throw;
            }
        }

        /// <summary> 
        /// This method creates a new impersonation context. 
        /// </summary> 
        public static Identity ImpersonateAppPoolIdentity() { return new Identity(); }

        public static Identity GetCurrentIdentity() { return new Identity(false); }

        /// <summary> 
        /// This method closes the current impersonation context in order revert the user 
        /// to his real principal. 
        /// </summary> 
        public void UndoImpersonation()
        {
            if (_context != null)
            {
                _context.Undo();
                _context = null;
            }
            if (_selfContext != null)
            {
                _selfContext.Undo();
                _selfContext = null;
            }
        }

        /// <summary> 
        /// Duplicates a token in order to have it working for impersonation. 
        /// </summary> 
        /// <param name="hToken_">Initial token to be duplicated</param> 
        /// <param name="impersonationLevel_">Level of impersonation needed</param> 
        /// <param name="hNewToken_">Reference to the new token created</param> 
        /// <returns>True if the call succeeded, false otherwise.</returns> 
        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool DuplicateToken(IntPtr hToken_, int impersonationLevel_, ref IntPtr hNewToken_);

        /// <summary> 
        /// Closes an unmanaged handle in order to free allocated resources. 
        /// </summary> 
        /// <returns>True if the call succeeded, false otherwise.</returns> 
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool CloseHandle(IntPtr handle);

        #region IDisposable Members

        /// <summary> 
        /// This method disposes the current object, it frees all resources used by this class. 
        /// </summary> 
        public void Dispose()
        {
            Dispose(true);
            // Ensure I'm garbage collected.  
            GC.SuppressFinalize(this);
        }

        /// <summary> 
        /// This method disposes the current object, it frees all resources used by this class. 
        /// </summary> 
        /// <param name="disposing_">Do actual disposing or not.</param> 
        private void Dispose(bool disposing_)
        {
            if (disposing_)
            {
                this.UndoImpersonation();
            }
        }

        #endregion
    }

}